Add benchmark for return or = vs ||=#133
Conversation
| end | ||
|
|
||
| def value2 | ||
| return @value if @value |
There was a problem hiding this comment.
return @value if defined? @value is a real world use case for avoiding undefined exception.
There was a problem hiding this comment.
In my case I can ensure that the instance variable is always defined.
If I change the test from if @value to if defined?(@value) && @value) then the benchmark results are different and ||= becomes the faster variant.
There was a problem hiding this comment.
if defined?(@value) && @value) is the equivalent of ||=. So, maybe it makes sense for full comparison of all the use cases:
# For trying again if nil and not sure if variable is defined
1. `||=`
2. `return @value if defined?(@value) && @value)`
# For trying again if nil and sure the variable is defined
3. `return @value if @value`
# For not trying again if nil and not sure if variable is defined
4. `return @value if defined? @value`
|
I'm not quiet sure what this benchmark is trying to show off. It's obvious that |
|
@ixti we got the following real world case: we are using the json-ld gem and want to frame a graph. In the following stackprof trace, we spend 26% of the running time in 2 methods, with 12.1% in $ stackprof tmp/stackprof-cpu-*.dump --text --limit 2
==================================
Mode: cpu(1000)
Samples: 107 (0.00% miss rate)
GC: 11 (10.28%)
==================================
TOTAL (pct) SAMPLES (pct) FRAME
18 (16.8%) 15 (14.0%) RDF::URI#==
13 (12.1%) 13 (12.1%) RDF::URI#valueHere is the code of $ stackprof tmp/stackprof-cpu-*.dump --text --method 'RDF::URI#value'
RDF::URI#value (/home/julien/RubymineProjects/rdf/lib/rdf/model/uri.rb:798)
samples: 13 self (12.1%) / 13 total (12.1%)
callers:
9 ( 69.2%) RDF::URI#to_str
4 ( 30.8%) RDF::URI#to_str
code:
| 798 | def value
| 799 | @value ||= [
| 800 | ("#{scheme}:" if absolute?),
| 801 | ("//#{authority}" if authority),
| 802 | path,
| 803 | ("?#{query}" if query),
| 804 | ("##{fragment}" if fragment)
13 (12.1%) / 13 (12.1%) | 805 | ].compact.join("").freeze
| 806 | endThe expensive build of the default value is only invoked once in our case (out of 10000s of calls to By adding an early return of the memoized variable if present, the time spent in the method went from 12% to 9% of the total, as show here: $ stackprof tmp/stackprof-cpu-*.dump --text --method 'RDF::URI#value'
RDF::URI#value (/home/julien/RubymineProjects/rdf/lib/rdf/model/uri.rb:798)
samples: 9 self (8.7%) / 9 total (8.7%)
callers:
6 ( 66.7%) RDF::URI#to_str
3 ( 33.3%) RDF::URI#to_str
code:
| 798 | def value
9 (8.7%) / 9 (8.7%) | 799 | return @value if @value
| 800 | @value ||= [ |
a98c882 to
d69d4d5
Compare
|
@texpert I have updated my benchmarks to take your comments into accounts, the results are interesting if we are sure that the memoized variable is defined. It seems that it is the $ ruby -v code/general/return-or-set-vs-or-equals.rb
ruby 2.4.1p111 (2017-03-22 revision 58053) [x86_64-linux-gnu]
Warming up --------------------------------------
||= 12.493k i/100ms
return @value if defined?(@value) && @value)
11.297k i/100ms
return @value if defined?(@value)
11.903k i/100ms
return @value if @value
16.584k i/100ms
Calculating -------------------------------------
||= 128.041k (± 2.7%) i/s - 649.636k in 5.077706s
return @value if defined?(@value) && @value)
112.480k (± 2.4%) i/s - 564.850k in 5.024635s
return @value if defined?(@value)
119.103k (± 2.5%) i/s - 595.150k in 5.000107s
return @value if @value
167.953k (± 2.3%) i/s - 845.784k in 5.038625s
Comparison:
return @value if @value: 167953.2 i/s
||=: 128041.4 i/s - 1.31x slower
return @value if defined?(@value): 119103.0 i/s - 1.41x slower
return @value if defined?(@value) && @value): 112480.4 i/s - 1.49x slower |
|
Oh. That's interesting. Although still a bit strange: class Memoizer
VALUE = "xxx"
def initialize
@value = nil
end
def return3
return @value if defined?(@value)
@value
end
endAt bare minimum |
|
In my tests |
|
I'm guessing it's slightly faster as |
|
@Arcovion that's good enough for my use-case, since I know that the variable exist. Updating my PR with your solution as the fastest. |
|
First of all, I want to emphasize that class X
def a
@a ||= 1
end
def b
@b || @b = 2
end
end
puts X.new.a
puts X.new.brun that with |
d69d4d5 to
acc35f2
Compare
|
Also, benchmarks are highly affected with require "benchmark/ips"
class Memoizer
VALUE = "some value".freeze
CYCLES = 10_000
def initialize
@v1 = nil
end
def v11
@v1 ||= VALUE
end
def v12
return @v1 if @v1
@v1 = VALUE
end
def v13
@v1 || @v1 = VALUE
end
def v21
@v2 ||= VALUE
end
def v22
return @v2 if defined?(@v2)
@v2 = VALUE
end
def v23
return @v2 if instance_variable_defined?(:@v2)
@v2 = VALUE
end
def self.example(name)
obj = new
obj.singleton_class.class_eval <<-RUBY
alias_method :run, :#{name}
def to_proc; proc { #{Array.new(CYCLES, "run").join(" ; ")} }; end
RUBY
obj
end
end
puts "== v1x"
Benchmark.ips do |x|
x.report("v11", &Memoizer.example(:v11))
x.report("v12", &Memoizer.example(:v12))
x.report("v13", &Memoizer.example(:v13))
x.compare!
end
puts "== v2x"
Benchmark.ips do |x|
x.report("v21", &Memoizer.example(:v21))
x.report("v22", &Memoizer.example(:v22))
x.report("v23", &Memoizer.example(:v23))
x.compare!
endwith the above, results are: |
The `||=` operator is commonly used to implement memoization, but when the memoization variable always exist some optimization is possible.
acc35f2 to
f93a559
Compare
|
|
||
| # For trying again if nil and not sure if variable is defined | ||
| def return1 | ||
| return @value if defined?(@value) && @value |
There was a problem hiding this comment.
There's no point in this test at all. @value is defined in initialize
|
|
||
| # For not trying again if nil and not sure if variable is defined | ||
| def return3 | ||
| return @value if defined?(@value) |
…ough ||=` See fastruby/fast-ruby#133 for discussion.
…ough ||=` See fastruby/fast-ruby#133 for discussion.
|
Closing this because there were comments with valid points that haven't been addressed in years. |
The
||=operator is commonly used to implement memoization.This benchmark shows a much faster alternative to implement memoization: explicit return when the memoized value should be reused, with fallback to setting the memoized value.